package de.hauschild.dbc4j.engine.groovy;

import groovy.lang.GroovyShell;

import org.codehaus.groovy.control.CompilationFailedException;

import de.hauschild.dbc4j.MalformedContractException;
import de.hauschild.dbc4j.annotation.Invariant;
import de.hauschild.dbc4j.annotation.PostCondition;
import de.hauschild.dbc4j.annotation.PreCondition;
import de.hauschild.dbc4j.engine.ContractEngine;

/**
 * A {@link ContractEngine} implementation using the Groovy scripting language.
 * <p>
 * The following keywords are available for contract expressions:
 * <ul>
 * <li><code>target</code> ({@link PreCondition}, {@link PostCondition}, {@link Invariant})
 * <ul>
 * <li>will bound to the target object of invocation</li>
 * </ul>
 * </li>
 * <li><code>args ({@link PreCondition}, {@link PostCondition}, {@link Invariant})</code>
 * <ul>
 * <li>will bound to the argument array of the invocation</li>
 * </ul>
 * </li>
 * <li><code>result ({@link PostCondition})</code>
 * <ul>
 * <li>will bound to the result of the method invocation</li>
 * </ul>
 * </li>
 * <li><code>thrown ({@link PostCondition})</code>
 * <ul>
 * <li>will bound to the thrown exception of the method invocation</li>
 * </ul>
 * </li>
 * </ul>
 * </p>
 * 
 * @since 1.0.0
 * @author Klaus Hauschild
 */
public class GroovyContractEngine implements ContractEngine {

  public static final String ENGINE_NAME = "groovy";

  private static final String TARGET_VARIABLE_NAME = "target";
  private static final String ARGUMENTS_VARIABLE_NAME = "args";
  private static final String RESULT_VARIABLE_NAME = "result";
  private static final String THROWN_VARIABLE_NAME = "thrown";

  /**
   * Bind arguments to shell.
   * 
   * @param shell
   *          the shell
   * @param arguments
   *          the arguments
   */
  private static void bindArguments(final GroovyShell shell, final Object[] arguments) {
    shell.setVariable(ARGUMENTS_VARIABLE_NAME, arguments);
  }

  /**
   * Bind result to shell.
   * 
   * @param shell
   *          the shell
   * @param result
   *          the result
   */
  private static void bindResult(final GroovyShell shell, final Object result) {
    shell.setVariable(RESULT_VARIABLE_NAME, result);
  }

  /**
   * Bind target to shell.
   * 
   * @param shell
   *          the shell
   * @param target
   *          the target
   */
  private static void bindTarget(final GroovyShell shell, final Object target) {
    shell.setVariable(TARGET_VARIABLE_NAME, target);
  }

  /**
   * Bind thrown to shell.
   * 
   * @param shell
   *          the shell
   * @param thrown
   *          the thrown
   */
  private static void bindThrown(final GroovyShell shell, final Throwable thrown) {
    shell.setVariable(THROWN_VARIABLE_NAME, thrown);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public boolean evaluate(final PostCondition postCondition, final Object target, final Object[] arguments,
      final Object result, final Throwable thrown) throws MalformedContractException {
    // create shell
    final GroovyShell shell = new GroovyShell();
    // pre bindings
    preBindings(shell, target, arguments);
    // post bindings
    postBindings(shell, result, thrown);
    // evaluate pre-conditions expression
    return evaluateExpression(shell, postCondition.value());
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public boolean evaluate(final PreCondition preCondition, final Object target, final Object[] arguments)
      throws MalformedContractException {
    // create shell
    final GroovyShell shell = new GroovyShell();
    // pre bindings
    preBindings(shell, target, arguments);
    // evaluate pre-conditions expression
    return evaluateExpression(shell, preCondition.value());
  }

  /**
   * Evaluate expression with the shell.
   * 
   * @param shell
   *          the shell
   * @param expression
   *          the expression
   * @return <code>true</code> if the expressions result is <code>true</code>,<br />
   *         <code>false</code> otherwise
   * @throws MalformedContractException
   *           is thrown if the expression has not the type of {@link Boolean} or malformed in the {@link GroovyShell}
   */
  private boolean evaluateExpression(final GroovyShell shell, final String expression)
      throws MalformedContractException {
    try {
      final Object result = shell.evaluate(expression);
      return (Boolean) result;
    } catch (final CompilationFailedException cfe) {
      throw new MalformedContractException(String.format("The contract '%s' is not valid.", expression), cfe);
    } catch (final ClassCastException cce) {
      throw new MalformedContractException(String.format("The contract '%s' has not boolean type.", expression), cce);
    }
  }

  /**
   * Bindings for {@link PostCondition} evaluation.
   * 
   * @param shell
   *          the shell
   * @param result
   *          the result
   * @param thrown
   *          the thrown
   */
  private void postBindings(final GroovyShell shell, final Object result, final Throwable thrown) {
    // result binding
    bindResult(shell, result);
    // thrown binding
    bindThrown(shell, thrown);
  }

  /**
   * Bindings for {@link PreCondition}, {@link PostCondition} and {@link Invariant} evaluation.
   * 
   * @param shell
   *          the shell
   * @param target
   *          the target
   * @param arguments
   *          the arguments
   */
  private void preBindings(final GroovyShell shell, final Object target, final Object[] arguments) {
    // target binding
    bindTarget(shell, target);
    // argument binding
    bindArguments(shell, arguments);
  }

}
